/* globals jQuery:false */
/**
 * A KBox type and a corresponding jQuery plugin.
 *
 * So, what is a kbox?
 * A kbox can be a modal dialog or a (dhtml, not window) popup or a ...
 * The kbox can be configured programatically or using data-* attributes.
 * Default styles are in kbox.css.
 *
 * Usage
 *      Declarative:
 *      <a id="example-id" ...>Click here to show modal</a>
 *      <div class="kbox" title="A modal dialog" data-target="#example-id" data-modal="true">
 *          .... modal content ....
 *      </div>
 *
 *      Programatic:
 *      var kbox = new KBox('<p>Some content</p>', {
 *          title: 'KBox Title'
 *      });
 *      kbox.open();
 *
 *      Mixed:
 *      [HTML]
 *      <a id="a-id" ...>Click ...</a>
 *      <div id="kbox-id" class="kbox" data-target="#a-id">...content...</div>
 *      [JavaScript]
 *      var kbox = $('kbox-id').data('kbox'); // Gets the kbox instance.
 *      kbox.updateOptions({
 *          preOpen: function() {
 *              // If isFormValid() returns false, the kbox doesn't open;
 *              return isFormValid();
 *          }
 *      });
 *
 * Options
 *      clickTarget / data-target:
 *          jQuery or DOM elements or CSS Selector to target(s)
 *          that will trigger the kbox to open on click. Optional.
 *      closeOnEsc / data-close-on-esc:
 *          Close the kbox on ESC. Default: true.
 *      closeOnOutClick: / data-close-on-out-click:
 *          Close the kbox on any click outside of it. Default: false.
 *      container / data-container:
 *          jQuery or DOM element for appending the kbox. Optional.
 *          If html string is passed as the content of the kbox, container
 *          will default to $('body').
 *      destroy / data-destroy:
 *          Clean up DOM changes on close. Default: false.
 *      id / data-id:
 *          An id for the kbox container. Optional.
 *      modal / data-modal:
 *          Do we need to make the kbox modal? Adds a background overlay.
 *          Default: false.
 *      position / data-position:
 *          Where to position the kbox.
 *              'center': (default) centers the kbox in the window
 *              'none': doesn't do any positioning so you can do it in CSS
 *              (TODO:) 'force-center': keeps kbox center as window scrolls
 *      preOpen:
 *          A function to call before opening the kbox. If the function
 *          returns false, the kbox isn't opened. Optional.
 *      preClose:
 *          A function to call before closing the kbox. If the function
 *          returns false, the kbox isn't closed. Optional.
 *      template:
 *          Override the template to use for creating the modal.
 *      title:
 *          The kbox's title.
 */

(function($) {

  'use strict'; // Giving this a shot!

  var TEMPLATE = (
      '<div class="kbox-container">' +
      '<a href="#close" class="kbox-close">&#x2716;</a>' +
      '<div class="kbox-title"></div>' +
      '<div class="kbox-wrap"><div class="kbox-placeholder"/></div>' +
      '</div>'
    ),
    OVERLAY = '<div id="kbox-overlay"></div>';

  // The KBox type
  function KBox(el, options) {
    KBox.prototype.init.call(this, el, options);
  }

  KBox.prototype = {
    init: function(el, options) {
      var self = this;
      self.el = el;
      self.html = typeof el === 'string' && el;
      self.$el = $(el);
      options = $.extend({
        // defaults
        clickTarget: self.$el.data('target'),
        closeOnEsc: self.$el.data('close-on-esc') === undefined ?
        true : !!self.$el.data('close-on-esc'),
        closeOnOutClick: !!self.$el.data('close-on-out-click'),
        container: self.html && $('body'),
        // TODO: maxHeight: self.$el.data('max-height') || 'window',
        destroy: !!self.$el.data('destroy'),
        id: self.$el.data('id'),
        modal: !!self.$el.data('modal'),
        position: self.$el.data('position') || 'center',
        preOpen: false,
        preClose: false,
        template: TEMPLATE,
        title: self.$el.attr('title') || self.$el.attr('data-title')
      }, options);
      self.options = options;
      self.$clickTarget = options.clickTarget && $(options.clickTarget);
      self.$container = options.container && $(options.container);
      self.rendered = false; // did we render out yet?
      self.$ph = false; // placeholder used if we need to move self.$el in the DOM.
      self.$kbox = $();
      self.isOpen = false;

      // Make the instance accessible from the DOM element.
      self.$el.data('kbox', self);

      // If we have a click target, open the kbox when it is clicked.
      if (self.$clickTarget) {
        self.$clickTarget.click(function(ev) {
          ev.preventDefault();
          self.open();
        });
      }

    },
    updateOptions: function(options) {
      // Ability to update options programmatically after kbox creation.
      var self = this;
      self.options = $.extend(self.options, options);
      self.$clickTarget = options.clickTarget && $(options.clickTarget);
      self.$container = options.container && $(options.container);
    },
    render: function() {
      var self = this;
      self.$kbox = $(self.options.template);

      if (self.$container) {
        // The kbox will be appended to the container.
        if (self.$el.parent().length) {
          // If we are attached to the DOM, save our place there
          // for putting everything back in place later.
          self.$ph = self.$el.before('<div style="display:none;"/>').prev();
        }
        self.$kbox.appendTo(self.$container);
      } else {
        // The kbox will go right where $el is.
        self.$el.before(self.$kbox);
      }

      // Set the id if it was specified
      if (self.options.id) {
        self.$kbox.attr('id', self.options.id);
      }

      // Set the title if it was specified.
      if (self.options.title) {
        self.$kbox.find('.kbox-title').text(self.options.title);
      }

      // Insert the content.
      self.$kbox.find('.kbox-placeholder').replaceWith(self.$el.detach());

      // Handle close events
      self.$kbox.delegate('.kbox-close, .kbox-cancel', 'click', function(ev) {
        ev.preventDefault();
        self.close();
      });

      self.rendered = true;
    },
    open: function() {
      var self = this;
      if (self.options.preOpen && !self.options.preOpen.call(self)) {
        // If we have a preOpen callback and it returns false,
        // we don't open anything.
        return;
      }
      if (self.isOpen) {
        return;
      }
      self.isOpen = true;
      if (!self.rendered) {
        self.render();
      }
      self.$kbox.addClass('kbox-open');
      self.setPosition();
      if (self.options.modal) {
        self.createOverlay();
      }

      // Handle ESC
      if (self.options.closeOnEsc) {
        self.keypressHandler = function(ev) {
          if (ev.keyCode === 27) {
            self.close();
          }
        };
        $(document).keypress(self.keypressHandler);
      }

      // Handle outside clicks
      if (self.options.closeOnOutClick) {
        self.clickHandler = function(ev) {
          if ($(ev.target).closest('.kbox-container').length === 0) {
            // The click isn't inside the kbox, so lets close it.
            self.close();
          }
        };
        setTimeout(function() { // so it doesn't get triggered on this click
          $('body').click(self.clickHandler);
        }, 0);
      }
    },
    setPosition: function(position) {
      var self = this,
        toX, toY, $parent, parentOffset, minX, minY, scrollL, scrollT;
      if (!position) {
        position = self.options.position;
      }
      if (position === 'none' || !self.$kbox.length) {
        return;
      }
      if (position === 'center') {
        $parent = self.$kbox.offsetParent();
        parentOffset = $parent.offset();
        scrollL = $(window).scrollLeft();
        scrollT = $(window).scrollTop();
        minX = -parentOffset.left + scrollL;
        minY = -parentOffset.top + scrollT;
        toX = ($(window).width() - self.$kbox.outerWidth()) / 2 - parentOffset.left + scrollL;
        toY = ($(window).height() - self.$kbox.outerHeight()) / 2 - parentOffset.top + scrollT;
        if (toX < minX) {
          toX = minX;
        }
        if (toY < minY) {
          toY = minY;
        }
        self.$kbox.css({
          'left': toX,
          'top': toY,
          'right': 'inherit',
          'bottom': 'inherit'
        });
      }
    },
    close: function() {
      var self = this;
      if (self.options.preClose && !self.options.preClose.call(self)) {
        // If we have a preClose callback and it returns false,
        // we don't open anything.
        return;
      }
      if (!self.isOpen) {
        return;
      }
      self.isOpen = false;
      self.$kbox.removeClass('kbox-open');
      if (self.options.modal) {
        self.destroyOverlay();
      }
      if (self.options.destroy) {
        self.destroy();
      }
      if (self.options.closeOnEsc) {
        $('body').unbind('keypress', self.keypressHandler);
      }
      if (self.options.closeOnOutClick) {
        $('body').unbind('click', self.clickHandler);
      }
    },
    destroy: function() {
      // return DOM to how it was originally, if possible.
      var self = this;
      if (self.$container && self.$ph) {
        self.$ph.replaceWith(self.$el.detach());
      }
      self.$kbox.remove();
    },
    createOverlay: function() {
      var self = this;
      self.$overlay = $(OVERLAY);
      self.$kbox.before(self.$overlay);
    },
    destroyOverlay: function() {
      if (this.$overlay) {
        this.$overlay.remove();
        delete this.$overlay;
      }
    }
  };

  // Create the jQuery plugin.
  $.fn.kbox = function(options) {
    return this.each(function() {
      new KBox(this, options); // eslint-disable-line
    });
  };

  // Expose KBox to the world, in case they don't want to use the plugin.
  window.KBox = KBox;

  // Initialize declared kboxes.
  $('.kbox').kbox();

})(jQuery);
